import { inject, injectable } from 'inversify';
import { URI as Uri } from 'vscode-uri';
import URI from '@theia/core/lib/common/uri';
import { Deferred } from '@theia/core/lib/common/promise-util';
import {
    FileSystemProvider,
    FileSystemProviderError,
    FileSystemProviderErrorCode,
} from '@theia/filesystem/lib/common/files';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { DelegatingFileSystemProvider } from '@theia/filesystem/lib/common/delegating-file-system-provider';
import {
    FileService,
    FileServiceContribution,
} from '@theia/filesystem/lib/browser/file-service';
import { AuthenticationClientService } from '../auth/authentication-client-service';
import { AuthenticationSession } from '../../common/protocol/authentication-service';
import { ConfigService } from '../../common/protocol';

export namespace LocalCacheUri {
    export const scheme = 'arduino-local-cache';
    export const root = new URI(
        Uri.parse('/').with({ scheme, authority: 'create' })
    );
}

@injectable()
export class LocalCacheFsProvider
    implements
        FileServiceContribution,
        DelegatingFileSystemProvider.URIConverter
{
    @inject(ConfigService)
    protected readonly configService: ConfigService;

    @inject(AuthenticationClientService)
    protected readonly authenticationService: AuthenticationClientService;

    // TODO: do we need this? Cannot we `await` on the `init` call from `registerFileSystemProviders`?
    readonly ready = new Deferred<void>();

    private _localCacheRoot: URI;

    registerFileSystemProviders(fileService: FileService): void {
        fileService.onWillActivateFileSystemProvider(async (event) => {
            if (event.scheme === LocalCacheUri.scheme) {
                event.waitUntil(
                    (async () => {
                        this.init(fileService);
                        const provider = await this.createProvider(fileService);
                        fileService.registerProvider(
                            LocalCacheUri.scheme,
                            provider
                        );
                    })()
                );
            }
        });
    }

    to(resource: URI): URI | undefined {
        const relativePath = LocalCacheUri.root.relative(resource);
        if (relativePath) {
            return this.currentUserUri.resolve(relativePath).normalizePath();
        }
        return undefined;
    }

    from(resource: URI): URI | undefined {
        const relativePath = this.currentUserUri.relative(resource);
        if (relativePath) {
            return LocalCacheUri.root.resolve(relativePath);
        }
        return undefined;
    }

    protected async createProvider(
        fileService: FileService
    ): Promise<FileSystemProvider> {
        const delegate = await fileService.activateProvider('file');
        await this.ready.promise;
        return new DelegatingFileSystemProvider(
            delegate,
            {
                uriConverter: this,
            },
            new DisposableCollection(
                delegate.watch(this.localCacheRoot, {
                    excludes: [],
                    recursive: true,
                })
            )
        );
    }

    protected async init(fileService: FileService): Promise<void> {
        const config = await this.configService.getConfiguration();
        this._localCacheRoot = new URI(config.dataDirUri);
        for (const segment of ['RemoteSketchbook', 'ArduinoCloud']) {
            this._localCacheRoot = this._localCacheRoot.resolve(segment);
            await fileService.createFolder(this._localCacheRoot);
        }
        this.session(fileService).then(() => this.ready.resolve());
        this.authenticationService.onSessionDidChange(async (session) => {
            if (session) {
                await this.ensureExists(session, fileService);
            }
        });
    }

    private get currentUserUri(): URI {
        const { session } = this.authenticationService;
        if (!session) {
            throw new FileSystemProviderError(
                'Not logged in.',
                FileSystemProviderErrorCode.NoPermissions
            );
        }
        return this.toUri(session);
    }

    private get localCacheRoot(): URI {
        return this._localCacheRoot;
    }

    private async session(
        fileService: FileService
    ): Promise<AuthenticationSession> {
        return new Promise<AuthenticationSession>(async (resolve) => {
            const { session } = this.authenticationService;
            if (session) {
                await this.ensureExists(session, fileService);
                resolve(session);
                return;
            }
            const toDispose = new DisposableCollection();
            toDispose.push(
                this.authenticationService.onSessionDidChange(
                    async (session) => {
                        if (session) {
                            await this.ensureExists(session, fileService);
                            toDispose.dispose();
                            resolve(session);
                        }
                    }
                )
            );
        });
    }

    private async ensureExists(
        session: AuthenticationSession,
        fileService: FileService
    ): Promise<URI> {
        const uri = this.toUri(session);
        const exists = await fileService.exists(uri);
        if (!exists) {
            await fileService.createFolder(uri);
        }
        return uri;
    }

    private toUri(session: AuthenticationSession): URI {
        // Hack: instead of getting the UUID only, we get `auth0|UUID` after the authentication. `|` cannot be part of filesystem path or filename.
        return this._localCacheRoot.resolve(session.id.split('|')[1]);
    }
}
